#pragma once

#include "data.hpp"
#include "httplib.h"

extern cloud::DataManager *_data;

//业务处理模块: 网络处理模块和业务处理进行了合并 （网络模块已经使用httplib完成了）
//1.搭建网络通信模块: 借助httplib完成
//2.业务请求处理
//文件上传请求: 备份客户端上传的文件，响应上传成功 
//文件列表请求: 客户端浏览器请求一个备份文件的展示页面，响应页面
//文件下载请求: 通过展示页面，点击下载，响应客户端要下载的文件数据

// 网络通信接口设计：约定好，客户端发送什么样的请求，我们给与什么样的响应
// 1.请求: 文件上传，展示页面，文件下载
// 接口设计:  

// 1.文件上传
// 当服务器收到了一个 post方法的/upload请求(与客户端约定好下载上传请求) 时，我们则认为这时一个文件上传请求
// 解析请求，得到文件数据，将数据写入到文件中，备份成功
// 返回响应 HTTP/1.1 200 OK

// 2.展示页面
// GET /listshow展示请求 HTTP/1.1 客户端请求格式 与 服务端约定

// 返回给客户端的响应  //服务端响应格式 与 客户端约定
// HTTP/1.1 200 OK
// Content-length: 文件长度  

// 正文就是Html页面，展现的文件列表

// 3.文件下载
// GET /download/test.txt 下载请求 HTTP/1.1
// ...

// 返回给客户端的响应
// HTTP/1.1 200 OK
// Content_length:文件长度

// 正文就是文件数据

// 都是通过响应正文将数据返回给客户端
namespace cloud
{
    //服务器业务处理类设计: 
    class Service
    {
        //搭建http服务器，并且进行处理
    private:
        int _server_port; //服务端端口
        std::string _server_ip; //服务端ip
        std::string _download_prefix; //下载前缀，带有/download就是一个下载请求
        httplib::Server _server; //搭建服务器

    private: 
        static void Upload(const httplib::Request &req, httplib::Response &rsp) // 上传功能
        {
            // post /upload 文件数据在正文中(正文并不全是文件数据)
            auto ret = req.has_file("file"); // 判断有没有上传的文件区域 file会与我们中的file字段查询到，也就是你对应要上传的文件
            if (ret == false) //没有，则失败
            {
                std::cout << "not file upload\n";
                rsp.status = 400;
                return;
            }

            const auto &file = req.get_file_value("file"); 
            std::string back_dir = Config::GetInstance()->GetBackDir(); //获取文件备份路径
            std::string realpath = back_dir + FileUtil(file.filename).FileName(); //文件实际路径 = 备份路径 + 文件名
            FileUtil fu(realpath);
            fu.SetContent(file.content); // 将数据写入文件当中;
            
            BackupInfo info;
            info.NewBackupInfo(realpath); // 组织我们备份的文件信息
            _data->Insert(info);          // 向数据管理模块添加备份的文件信息

            return;
        }

        static std::string TimetoStr(time_t t)
        {
            std::string tmp = std::ctime(&t); //将时间变成一种格式
            return tmp;
        }

        static void ListShow(const httplib::Request &req, httplib::Response &rsp) // 展示功能
        {
            // 1. 获取所有的文件备份信息
            std::vector<BackupInfo> arry;
            _data->GetAll(&arry);
            // 2. 根据所有备份信息，组织html文件数据
            std::stringstream ss;
            ss << "<html><head><title>Download</title></head>";
            ss << "<body><h1>Download</h1><table>";
            for (auto &a : arry)
            {
                 // 遍历列表时，更新每个数据的内存访问时间
                _data->UpdateMemAtime(a.url);
                ss << "<tr>";
                std::string filename = FileUtil(a.real_path).FileName();
                ss << "<td><a href='" << a.url << "'>" << filename << "</a></td>";
                ss << "<td align='right'>" << TimetoStr(a.mtime) << "</td>";
                ss << "<td align='right'>" << a.fsize / 1024 << "K" << "</td>";
                ss << "</tr>";
            }
            ss << "</table></body></html>";
            rsp.body = ss.str(); //设置响应正文
            rsp.set_header("Content-Type", "text/html"); //设置响应报头
            rsp.status = 200; //设置状态码
            return;
        }

        //http的ETag头部字段: 其中存储了一个资源的唯一标识
        //客户端第一次下载文件的时候，会收到这个响应信息
        //第二次下载，就会将这个信息发送给服务器，想要让服务器根据这个唯一标识判断文件有没有被修改过
        //如果没有被修改，直接使用原先缓存的数据，不用重新下载了

        //http协议本身对于etag中是什么数据并不关心，只要你服务器能够自己标识即可
        //因此我们etag可以使用 "文件名 + 文件大小 + 文件最后一次修改时间" 组成
        //etag字段不仅仅是缓存用到，还有就是后边的断点续传的实现也会用到
        //断点续传也需要保证文件没有被修改过，如果被修改过那么继续断点续传也没有意义

        //总结:
        // ETag 由 "文件名 + 文件大小 + 文件最后一次修改时间" 组成
        // http的头部字段Etag: 其中储存了一个资源的唯一标识
        // 1.第一次下载时，客户端会收到这个响应信息，第二次下载时，客户端会将这个信息发送给服务器，如果ETag这个唯一标识没有改变的话，就直接使用原先缓存的数据响应给客户端，就不需要重新下载了
        // 2.在实现断点续传时也需要判断文件是否被修改过，如果修改过那么继续断点续传也没有意义了

        //http协议的Accept-Ranges: bytes字段: 用于告诉客户端我支持断点续传,并且数据单位以字节作为单位
        static std::string GetETag(const BackupInfo &info)
        {
            // etg: filename-fsize-mtime 通过 文件名 文件大小 文件最后一次修改时间
            FileUtil fu(info.real_path);
            std::string etag = fu.FileName(); //文件名
            etag += "-";
            etag += std::to_string(info.fsize); //文件大小
            etag += "-";
            etag += std::to_string(info.mtime); //文件最后一次修改时间
            return etag;
        }

        //获取url中path，然后获取该文件，查看该文件是否被压缩，如果没有压缩，则直接拿取文件数据，压缩，则先解压
        static void Download(const httplib::Request &req, httplib::Response &rsp) // 下载功能
        {
            // 1.获取客户端请求的资源路径path req.path
            // 2.根据资源路径，获取文件备份信息
            BackupInfo info;
            _data->GetOneByURL(req.path, &info);
            _data->UpdateMemAtime(req.path);
            // 3.判断文件是否被压缩，如果被压缩，要先解压缩
            if (info.pack_flag == true)
            {
                // 将文件解压到备份路径下 
                FileUtil fu(info.pack_path); //使用压缩路径进行初始化
                fu.UnCompress(info.real_path); //使其解压到备份目录下
                // 4.删除压缩包，修改备份信息（已经没有被压缩）
                fu.Remove(); //删除压缩包
                info.pack_flag = false; //修改标识信息
                _data->Update(info); //更新文件
            }

            bool retrans = false;
            std::string old_etag;
            //如果在请求报头中有If-Range字段 则表示这次下载是断点续传 
            //且这个字段的值与请求文件的最新etag一致则符合断点续传 保证文件没有被修改过
            //符合以上两点则进行断点续传
            if (req.has_header("If-Range")) //首先判断头部字段当中是否有If-Range这个字段
            {
                old_etag = req.get_header_value("If_Range");
                if (old_etag == GetETag(info)) //如果两次文件数据相同，则符合断点续传的规则
                {
                    retrans = true;
                }
            }
            //没有，则表示该下载请求属于正常下载请求
            // 如果没有If-Range字段则是正常下载，或者如果有这个字段，但是
            // 它的值与当前文件的etag不一致，则也必须重新返回全部数据
            // 5.读取文件数据，放入rsp.body中
            FileUtil fu(info.real_path);

            if (retrans == false)
            {
                fu.GetContent(&rsp.body);
                // 6.设置响应头部字段：ETag, Accept-Ranges: bytes
                rsp.set_header("Accept-Ranges", "bytes");
                rsp.set_header("Etag", GetETag(info));
                rsp.set_header("Content-Type", "addlication/octet-stream");
                rsp.status = 200;
            } 
            // 断点续传的实现
            // 功能: 当文件下载过程中，因为某种异常而中断，如果再次进行从头下载，效率较低，因为需要将之前已经传输过的数据在次传输一遍
            // 断点续传就是从上次下载断开的位置，重新下载即可，之前已经传输过的数据不需要在重新传输
            
            //目的: 提高文件的重新传输效率
            //实现思想: 客户端在下载文件的时候，当每次接收到数据写入文件后记录自己当前下载的数据量
            //当异常下载中断时，下次断点续传的时候，将要重新下载的数据的区间（下载起始位置，结束位置）
            //发送给服务器，服务器收到后，仅仅回传客户端需要的区间数据即可

            //需要考虑的问题:
            //如果上次下载文件之后，这个文件在服务器上被修改了，则这时候不能重新断点续传，而是应该重新进行文件的下载操作
            
            //在http协议中断点续传的实现: 
            //1.主要关键点在于能够告诉服务器下载区间范围，
            //2.以及服务器上要能检测上一次下载之后的文件是否被修改过
            //实现:

            // 文件下载:
            // GET/download/test.txt HTTP/1.1
            // ...

            // 服务端响应 
            // HTTp/1.1 200 OK
            // Accept-Ranges: bytes 告诉客户端服务器支持断点续传功能
            // ETag: "dgbwadgajdf-文件唯一标识" 客户端收到响应会保存这个信息
            // ....
            // 文件数据作为正文

            // 断点续传(客户端请求): 
            // GET /download/test.txt HTTP/1.1
            // If-Range："服务端在下载时etag字段" 用于服务端判断这个文件与 原先下载的文件是否一致 不一致则重新完成文案的下载
            // 如果一致我们就响应100-10000这个区间的数据给服务端
            // Range:bytes 100-10000(区间范围) 这个字段用于告诉服务器客户端需要的数据区间范围

            //服务器成功处理断点续传之后(服务端响应):
            // HTTP/1.1 206 partial content(成功处理部分GET请求)
            // ETag: "dagwjkfuga"
            // Content-Range:bytes 100-10000/文件大小 100请求起始位置 10000请求的结束位置
            // 正文就是服务端响应给客户端对应的区间数据

            //Range:bytes start-end
            //      bytes 100
            else 
            {
                //httplib内部实现了对于区间请求也就是断点续传请求的处理
                //只需要我们用户将文件所有数据读取到rsp.body中，它内部会自动根据请求
                //区间，从body中取出指定区间数据进行响应
                //std::string range = req.get_header_val("Range"); bytes=start-end
                fu.GetContent(&rsp.body);
                rsp.set_header("Accept-Ranges", "bytes start-end/fsize");
                rsp.set_header("Etag", GetETag(info));
                rsp.set_header("Content-Range", "bytes start-end/fsize");
                //rsp.set_header("Content-Range", "bytes");
                rsp.status = 206; //区间请求响应的是206******  206: 服务器成功处理了部分GET请求 区间请求
            }
        }

    public:
        //初始化
        Service()
        {
            Config *config = Config::GetInstance();
            _server_port = config->GetServerPort();
            _server_ip = config->GetServerIp();
            _download_prefix = config->GetDownloadPrefix();
        }
        //运行模块
        bool RunModule()
        {
            //搭建服务器
            _server.Post("/upload", Upload); //文件上传
            _server.Get("/listshow", ListShow); //展示列表
            _server.Get("/", ListShow); //没有任何的资源路径请求，我们则认为是请求展示列表
            std::string download_url = _download_prefix + "(.*)"; //匹配任何文件的下载请求 （.*）匹配任意一个字符任意次
            _server.Get(download_url, Download); //文件下载
            _server.listen(_server_ip.c_str(), _server_port); //运行服务器

            return true;
        }
    }; 
}
